
package scanner

import "unicode"

// Scanner contains array of characters and current position
type Scanner struct {
	text []rune
	pos int
}

func New(txt string) *Scanner {
	return &Scanner{[]rune(txt+"@"), 0}
}

// Constants for tokens
const (
	None byte = iota
	Number  // Digit{Digit}
	Ident   // Letter{Letter|Digit}
	Add     // '+'
	Sub     // '-'
	Mul     // '*'
	Div     // '/'
	LPar    // '('
	RPar    // ')'
	Equal   // '='
	Print   // 'print', '?'
	Eos     // ';'
)

var keywords = map[string]byte {
  "print": Print }

// Helper method to scan characters with given predicate
func (s* Scanner) scanWith(predicate func(r rune) bool) string {
  k := s.pos
  for predicate(s.text[s.pos]) { s.pos++ }
  return string(s.text[k:s.pos])
}

func isLetterOrDigit(r rune) bool {
  return unicode.IsLetter(r) || unicode.IsDigit(r)
}

// Scan new lexeme and return it with token
func (s *Scanner) Next() (string, byte) {
  // skip whitespaces
  s.scanWith(unicode.IsSpace)

  ch := s.text[s.pos]
  // Scan numbers
  if unicode.IsDigit(ch) {
    return s.scanWith(unicode.IsDigit), Number
  }

  // identifiers
  if unicode.IsLetter(ch) {
    iden := s.scanWith(isLetterOrDigit)
    tok, iskey := keywords[iden]
    if !iskey { tok = Ident }
    return iden, tok
  }

  // scan symbols
  var res byte = None
  s.pos++
  switch ch {
    case '+': res = Add
    case '-': res = Sub
    case '*': res = Mul
    case '/': res = Div
    case '(': res = LPar
    case ')': res = RPar
    case '=': res = Equal
    case '?': res = Print
    case '@': res = Eos
  }

	return string(ch), res
}

